/*
 * Copyright (C) 2009 Rafael Fernandes
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.phonebooksharing.android.widgets.scroll;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Scroller;

import com.phonebooksharing.android.widgets.pagination.PaginatorListView;

/**
 * @author Rafael Fernandes
 */
public class HorizontalScroller extends ScrollableLayer {
    private static final int INVALID_SCREEN = -1;

    /**
     * The velocity at which a fling gesture will cause us to snap to the next screen
     */
    private static final int SNAP_VELOCITY = 1000;
    private boolean firstLayout = true;
    private Scroller scroller;
    private VelocityTracker velocityTracker;
    private float mLastMotionX, mLastMotionY;
    private final static int TOUCH_STATE_REST = 0, TOUCH_STATE_SCROLLING = 1;
    private int defaultScreen = 0, currentScreen, nextScreen = INVALID_SCREEN, touchState = TOUCH_STATE_REST;
    private boolean allowLongPress;

    /**
     * Used to inflate the Workspace from XML.
     *
     * @param context The application's context.
     * @param attrs The attribtues set containing the Workspace's customization values.
     */
    public HorizontalScroller(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * Initializes various states for this scroller.
     */
    private void init() {
        scroller = new Scroller(getContext());
        currentScreen = defaultScreen;
    }

    /**
     * Sets the current screen.
     *
     * @param cs
     */
    public void setCurrentScreen(int cs) {
        currentScreen = Math.max(0, Math.min(cs, getChildCount() - 1));
        scrollTo(cs * getWidth(), 0);
        invalidate();
    }

    /**
     * Shows the default screen
     */
    public void showDefaultScreen() {
        setCurrentScreen(defaultScreen);
    }
    
    /**
     * Sets the default screen
     * @param ds
     */
    public void setDefaultScreen(int ds) {
        defaultScreen = ds;
        showDefaultScreen();
    }

    /**
     * Registers the specified listener on each screen contained in this workspace.
     *
     * @param l The listener used to respond to long clicks.
     */
    @Override
    public void setOnLongClickListener(OnLongClickListener l) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            getChildAt(i).setOnLongClickListener(l);
        }
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
//            mScrollX = mScroller.getCurrX();
//            mScrollY = mScroller.getCurrY();
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        } else if (nextScreen != INVALID_SCREEN) {
            currentScreen = Math.max(0, Math.min(nextScreen, getChildCount() - 1));
            nextScreen = INVALID_SCREEN;
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        // ViewGroup.dispatchDraw() supports many features we don't need:
        // clip to padding, layout animation, animation listener, disappearing
        // children, etc. The following implementation attempts to fast-track
        // the drawing dispatch by drawing only what we know needs to be drawn.

        boolean fastDraw = touchState != TOUCH_STATE_SCROLLING && nextScreen == INVALID_SCREEN;
        // If we are not scrolling or flinging, draw only the current screen
        if (fastDraw) {
            drawChild(canvas, getChildAt(currentScreen), getDrawingTime());
        } else {
            final long drawingTime = getDrawingTime();
            // If we are flinging, draw only the current screen and the target screen
            if (nextScreen >= 0 && nextScreen < getChildCount() &&
                    Math.abs(currentScreen - nextScreen) == 1) {
                drawChild(canvas, getChildAt(currentScreen), drawingTime);
                drawChild(canvas, getChildAt(nextScreen), drawingTime);
            } else {
                // If we are scrolling, draw all of our children
                final int count = getChildCount();
                for (int i = 0; i < count; i++) {
                    drawChild(canvas, getChildAt(i), drawingTime);
                }
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int width = MeasureSpec.getSize(widthMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("HorizontalScroller can only be used in EXACTLY mode.");
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("HorizontalScroller can only be used in EXACTLY mode.");
        }

        // The children are given the same width and height as the workspace
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
        }

        if (firstLayout) {
            scrollTo(currentScreen * width, 0);
            firstLayout = false;
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childLeft = 0;

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                final int childWidth = child.getMeasuredWidth();
                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    @Override
    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
        int screen = indexOfChild(child);
        if (screen != currentScreen || !scroller.isFinished()) {
            snapToScreen(screen);
            return true;
        }
        return false;
    }

    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        int focusableScreen;
        if (nextScreen != INVALID_SCREEN) {
            focusableScreen = nextScreen;
        } else {
            focusableScreen = currentScreen;
        }
        getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect);
        return false;
    }

    @Override
    public boolean dispatchUnhandledMove(View focused, int direction) {
        if (direction == View.FOCUS_LEFT) {
            if (currentScreen > 0) {
                snapToScreen(currentScreen - 1);
                return true;
            }
        } else if (direction == View.FOCUS_RIGHT) {
            if (currentScreen < getChildCount() - 1) {
                snapToScreen(currentScreen + 1);
                return true;
            }
        }
        return super.dispatchUnhandledMove(focused, direction);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onTouchEvent will be called and we do the actual
         * scrolling there.
         */

        /*
         * Shortcut the most recurring case: the user is in the dragging
         * state and he is moving his finger.  We want to intercept this
         * motion.
         */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (touchState != TOUCH_STATE_REST)) {
            return true;
        }

        final float x = ev.getX();
        final float y = ev.getY();

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                /*
                 * Locally do absolute value. mLastMotionX is set to the y value
                 * of the down event.
                 */
                final int xDiff = (int) Math.abs(x - mLastMotionX);
                final int yDiff = (int) Math.abs(y - mLastMotionY);
                final int touchSlop = ViewConfiguration.getTouchSlop();
                
                boolean xMoved = xDiff > touchSlop;
                boolean yMoved = yDiff > touchSlop;
                
                if (xMoved || yMoved) {
                    if (xMoved) {
                        // Scroll if the user moved far enough along the X axis
                        touchState = TOUCH_STATE_SCROLLING;
                    }
                    // Either way, cancel any pending longpress
                    if (allowLongPress) {
                        allowLongPress = false;
                        // Try canceling the long press. It could also have been scheduled
                        // by a distant descendant, so use the mAllowLongPress flag to block
                        // everything
                        final View cs = getChildAt(currentScreen);
                        cs.cancelLongPress();
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                // Remember location of down touch
                mLastMotionX = x;
                mLastMotionY = y;
                allowLongPress = true;

                /*
                 * If being flinged and user touches the screen, initiate drag;
                 * otherwise don't.  mScroller.isFinished should be false when
                 * being flinged.
                 */
                touchState = scroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                touchState = TOUCH_STATE_REST;
                break;
        }

        /*
         * The only time we want to intercept motion events is if we are in the
         * drag mode.
         */
        return touchState != TOUCH_STATE_REST;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(ev);

        final int action = ev.getAction();
        final float x = ev.getX();

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            /*
             * If being flinged and user touches, stop the fling. isFinished
             * will be false if being flinged.
             */
            if (!scroller.isFinished()) {
                scroller.abortAnimation();
            }

            // Remember where the motion event started
            mLastMotionX = x;
            break;
        case MotionEvent.ACTION_MOVE:
            if (touchState == TOUCH_STATE_SCROLLING) {
                // Scroll to follow the motion event
                final int deltaX = (int) (mLastMotionX - x);
                mLastMotionX = x;

                if (deltaX < 0) {
                    if (getScrollX() > 0) {
                        //TODO: any performance issue here once this is fired for each touche movement
                        initListView(currentScreen - 1);
                        
                        scrollBy(Math.max(-getScrollX(), deltaX), 0);
                    }
                } else if (deltaX > 0) {
                    final int availableToScroll = getChildAt(getChildCount() - 1).getRight() -
                    getScrollX() - getWidth();
                    
                    if (availableToScroll > 0) {
                        //TODO: any performance issue here once this is fired for each touche movement
                        initListView(currentScreen + 1);
                        
                        scrollBy(Math.min(availableToScroll, deltaX), 0);
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if (touchState == TOUCH_STATE_SCROLLING) {
                final VelocityTracker vt = velocityTracker;
                vt.computeCurrentVelocity(1000);
                int velocityX = (int) vt.getXVelocity();

                if (velocityX > SNAP_VELOCITY && currentScreen > 0) {
                    // Fling hard enough to move left
                    snapToScreen(currentScreen - 1);
                } else if (velocityX < -SNAP_VELOCITY && currentScreen < getChildCount() - 1) {
                    // Fling hard enough to move right
                    snapToScreen(currentScreen + 1);
                } else {
                    snapToDestination();
                }

                if (velocityTracker != null) {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
            }
            touchState = TOUCH_STATE_REST;
            break;
        case MotionEvent.ACTION_CANCEL:
            touchState = TOUCH_STATE_REST;
        }

        return true;
    }
    
    public void initListView(int screen) {
        final ViewGroup vg = (ViewGroup)getChildAt(Math.max(0, Math.min(screen, getChildCount() - 1)));
        PaginatorListView plv = null;
        if(vg instanceof PaginatorListView) {
            plv = (PaginatorListView)vg;
        } else if( vg instanceof ListView ) {
            return;
        } else {
            View v = ((ViewGroup)vg.findViewById(android.R.id.list)).getChildAt(0);
            if(v instanceof PaginatorListView) {
                plv = (PaginatorListView)v;
            }
        }
        if(plv != null) plv.load();
    }

    private void snapToDestination() {
        final int screenWidth = getWidth();
        final int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;

        snapToScreen(whichScreen);
    }

    public void snapToScreen(int whichScreen) {
        whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
        boolean changingScreens = whichScreen != currentScreen;
        
        nextScreen = whichScreen;
        
        View focusedChild = getFocusedChild();
        if (focusedChild != null && changingScreens && focusedChild == getChildAt(currentScreen)) {
            focusedChild.clearFocus();
        }
        
        final int newX = whichScreen * getWidth();
        final int delta = newX - getScrollX();
        scroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 2);
        invalidate();
    }

    @Override
    public void scrollLeft() {
        if (nextScreen == INVALID_SCREEN && currentScreen > 0 && scroller.isFinished()) {
            snapToScreen(currentScreen - 1);
        }
    }

    @Override
    public void scrollRight() {
        if (nextScreen == INVALID_SCREEN && currentScreen < getChildCount() -1 &&
                scroller.isFinished()) {
            snapToScreen(currentScreen + 1);
        }
    }
    
    /**
     * @return True is long presses are still allowed for the current touch
     */
    public boolean allowLongPress() {
        return allowLongPress;
    }
}